package fr.p10.miage.m1.sr.tcpserver;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * <p>Traitement de chaque connexion client par un thread.</p>
 * <p>Sequence d'interactions :</p>
 * <ol><li>Thread lit message du client, horodate ce message et l'insère dans une table associative (Map).</li>
 * <li>Puis, il récupère des tables des autres thread les messages qu'ils ont recu 
 * depuis la derniere fois que son client a envoye un message.</li>
 * </ol>
 * <p>L'utilisation des tables associatives n'est pas strictement necessaire dans cette version.
 * Mais on peut imaginer une extension de cette application dans laquelle le client
 * souhaite recuperer un message d'une date donnee, connaissant cette date.
 * </p>
 * @author lom
 */
public class SocketThread implements Runnable {

    /** Tous les messages client reçus par chaque thread. 
     * <ul><li>Key: Thread Id;</li>
     * <li>Value:<ul> <li>Date du message,</li> <li>message</li></ul></li>
     * </ul>
     */
    private static final Map<Long, Map<Date, String>> ALLMESSAGES =
            Collections.synchronizedMap(new HashMap<Long, Map<Date, String>>());
    /**
     * New line.
     */
    private static final String NL = "\n";
    /**
     * Chaine pour annoncer les autres messages arrives depuis 
     * le dernier message recu.
     */
    private static final String SINCE = "Messages Since last time:";
    /**
     * Chaine pour annoncer qu'il n'y a eu aucun nouveau message depuis le dernier 
     * message recu.
     */
    private static final String NOSINCE = "No new message since last time";
    /**
     * Good bye message.
     */
    private static final String BYE = "BYE";
    /**
     * Tous les messages recus par le thread courant.
     */
    private Map<Date, String> myMsg;
    /**
     * Thread serveur pour un client.
     */
    private Thread thr;
    /**
     * Recevra le socket liant le thread serveur au client.
     */
    private Socket sock;
    /**
     * Pour la gestion du flux de sortie.
     */
    private DataOutputStream sortie;
    /**
     * Pour la gestion du flux d'entree.
     */
    private BufferedReader entree;
    /**
     * Date du dernier message recu.
     */
    private Date lastTime;

    public SocketThread(Socket accept) {
        sock = accept;
        try {
            /* Fabrication flux IO */
            sortie = new DataOutputStream(sock.getOutputStream());
            entree = new BufferedReader(new InputStreamReader(sock.getInputStream()));
        } catch (IOException e) {
            Logger.getLogger(SocketThread.class.getName()).log(Level.SEVERE, null, e);
            return;
        }
        myMsg = Collections.synchronizedMap(new HashMap<Date, String>());
        /* Instanciation du thread */
        thr = new Thread(this);
        /* Demarrage thread, methode run(), contient interactions avec client.*/
        thr.start();
    }

    @Override
    public void run() {
        StringBuilder otherMesg = new StringBuilder();
        StringBuilder clientMesg = new StringBuilder();

        SocketThread.registerMyMesgMap(myMsg, Thread.currentThread().getId());
        System.err.println("Started thread " + Thread.currentThread().getId());

        try {
            try {
                /*Read message from client, set its timestamp and store it*/
                String clientSentence = entree.readLine().trim();
                while (!clientSentence.equalsIgnoreCase(BYE)) {
                    Date myClDate = new Date();
                    clientMesg.append(sock.getInetAddress().getCanonicalHostName()).append("#")
                            .append(sock.getPort()).append("=").append(clientSentence);
                    System.out.println("FROM THREAD#" + Thread.currentThread().getId() + ": "
                            + clientMesg.toString() + " at " + myClDate.toString());
                    myMsg.put(myClDate, clientMesg.toString());

                    /* Construct Messages from other clients*/
                    otherMesg.append(SINCE).append(NL);
                    Set<Map.Entry<Long, Map<Date, String>>> allThreadMsgEntries = ALLMESSAGES.entrySet();
                    Iterator<Map.Entry<Long, Map<Date, String>>> it = allThreadMsgEntries.iterator();
                    Map.Entry<Long, Map<Date, String>> aThreadMapEntry;
                    while (it.hasNext()) {
                        aThreadMapEntry = it.next();
                        if (aThreadMapEntry.getKey() != Thread.currentThread().getId()) {
                            Map<Date, String> threadEntryMesg = aThreadMapEntry.getValue();
                            Set<Map.Entry<Date, String>> msgEntries = threadEntryMesg.entrySet();
                            if (lastTime == null) {// 1ere fois : recuperer tous les anciens messages
                                for (Map.Entry<Date, String> unMsg : msgEntries) {
                                    otherMesg.append(unMsg.getValue()).append(NL);
                                }
                            } else { // Pas la premiere fois
                                Iterator<Map.Entry<Date, String>> itDateMsg = msgEntries.iterator();
                                Map.Entry<Date, String> aMsgEntry;
                                while (itDateMsg.hasNext()) {
                                    aMsgEntry = itDateMsg.next();
                                    if (aMsgEntry.getKey().getTime() > lastTime.getTime()) {
                                        otherMesg.append(aMsgEntry.getValue()).append(NL);
                                    }
                                }
                            }
                        }
                    }
                    /* Prepare and send response */
                    lastTime = myClDate;
                    if (otherMesg.length() - 1 == SINCE.length()) {
                        otherMesg.delete(0, otherMesg.length());
                        otherMesg.append(NOSINCE).append(NL);
                    }
                    sortie.writeBytes(otherMesg.toString());
                    sortie.writeBytes(NL);
                    sortie.flush();
                    otherMesg.delete(0, otherMesg.length());
                    clientMesg.delete(0, clientMesg.length());

                    clientSentence = entree.readLine().trim();

                }// Fin while
            } catch (NullPointerException npe) {// Je triche un peu : cas de deconnection brutale du client.
                System.err.println("FROM THREAD#" + Thread.currentThread().getId()
                        + ": client probably disconnected abruptly. Stopping.");
            } finally {
                sock.close();
                sortie.close();
                entree.close();
            }
        } catch (IOException ex) {
            Logger.getLogger(SocketThread.class.getName()).log(Level.SEVERE, null, ex);
        }
        System.err.println("THREAD#" + Thread.currentThread().getId() + ": " + BYE);
    }

    /**
     * Register each thread received messages table into to the global one.
     * @param msgMap the message table to register
     * @param thID  the thread ID
     */
    public static void registerMyMesgMap(Map<Date, String> msgMap, Long thID) {
        SocketThread.ALLMESSAGES.put(thID, msgMap);
    }
}
